前言
接下来的几篇我们来讲解一下spring boot 中如何集成spring cache. spring cache 中支持如下cache:
- ConcurrentMap Cache
- Caffeine Cache
- EhCache
- GuavaCache(1.5版本废弃)
- Hazelcast Cache
- Infinispan Cache
- JCache Cache
我们只讲解ConcurrentMapCache和EhCache,其他的cache感兴趣的可以查阅相关资料.本文首先来看下ConcurrentMapCache.
ConcurrentMapCache集成
在pom文件中加入如下依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
在启动类加上@EnableCaching注解开始spring cache的支持.
由于加入cache,为了模拟的方便,我们加入mybatis,pom文件加入如下配置:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency>
在application.properties中加入如下配置:
mybatis.mapper-locations=classpath:com/example/demo/mapper/*.xml
在启动类中加入如下注解即可:
@MapperScan("com.example.demo.mapper")
UserMapper如下:
package com.example.demo.mapper; import com.example.demo.model.User; public interface UserMapper { int deleteByPrimaryKey(Long id); int insert(User record); int insertSelective(User record); User selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(User record); int updateByPrimaryKey(User record); }
User,代码如下:
package com.example.demo.model; import java.util.Date; public class User { private Long id; private String nickName; private String emailNew; private Date registerTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName == null ? null : nickName.trim(); } public String getEmailNew() { return emailNew; } public void setEmailNew(String emailNew) { this.emailNew = emailNew == null ? null : emailNew.trim(); } public Date getRegisterTime() { return registerTime; } public void setRegisterTime(Date registerTime) { this.registerTime = registerTime; } }
UserMapper.xml 如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.example.demo.mapper.UserMapper" > <resultMap id="BaseResultMap" type="com.example.demo.model.User" > <id column="id" property="id" jdbcType="BIGINT" /> <result column="nick_name" property="nickName" jdbcType="VARCHAR" /> <result column="email_new" property="emailNew" jdbcType="VARCHAR" /> <result column="register_time" property="registerTime" jdbcType="TIMESTAMP" /> </resultMap> <sql id="Base_Column_List" > id, nick_name, email_new, register_time </sql> <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" > select <include refid="Base_Column_List" /> from user where id = #{id,jdbcType=BIGINT} </select> <delete id="deleteByPrimaryKey" parameterType="java.lang.Long" > delete from user where id = #{id,jdbcType=BIGINT} </delete> <insert id="insert" parameterType="com.example.demo.model.User" > insert into user (id, nick_name, email_new, register_time) values (#{id,jdbcType=BIGINT}, #{nickName,jdbcType=VARCHAR}, #{emailNew,jdbcType=VARCHAR}, #{registerTime,jdbcType=TIMESTAMP}) </insert> <insert id="insertSelective" parameterType="com.example.demo.model.User" > insert into user <trim prefix="(" suffix=")" suffixOverrides="," > <if test="id != null" > id, </if> <if test="nickName != null" > nick_name, </if> <if test="emailNew != null" > email_new, </if> <if test="registerTime != null" > register_time, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides="," > <if test="id != null" > #{id,jdbcType=BIGINT}, </if> <if test="nickName != null" > #{nickName,jdbcType=VARCHAR}, </if> <if test="emailNew != null" > #{emailNew,jdbcType=VARCHAR}, </if> <if test="registerTime != null" > #{registerTime,jdbcType=TIMESTAMP}, </if> </trim> </insert> <update id="updateByPrimaryKeySelective" parameterType="com.example.demo.model.User" > update user <set > <if test="nickName != null" > nick_name = #{nickName,jdbcType=VARCHAR}, </if> <if test="emailNew != null" > email_new = #{emailNew,jdbcType=VARCHAR}, </if> <if test="registerTime != null" > register_time = #{registerTime,jdbcType=TIMESTAMP}, </if> </set> where id = #{id,jdbcType=BIGINT} </update> <update id="updateByPrimaryKey" parameterType="com.example.demo.model.User" > update user set nick_name = #{nickName,jdbcType=VARCHAR}, email_new = #{emailNew,jdbcType=VARCHAR}, register_time = #{registerTime,jdbcType=TIMESTAMP} where id = #{id,jdbcType=BIGINT} </update> </mapper>
UserService如下:
package com.example.demo.service; import com.example.demo.model.User; public interface UserService { int insertSelective(User record); User selectByPrimaryKey(Long id); User updateByPrimaryKeySelective(User record); int deleteByPrimaryKey(Long id); }
UserServiceImpl 如下:
package com.example.demo.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import com.example.demo.mapper.UserMapper; import com.example.demo.model.User; @Service public class UserServiceImpl implements UserService{ private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); @Autowired private UserMapper userMapper; @Override public int insertSelective(User record) { return userMapper.insertSelective(record); } @Override @Cacheable(cacheNames="users",key="#id") public User selectByPrimaryKey(Long id) { logger.info("查询数据库"); return userMapper.selectByPrimaryKey(id); } @Override @CachePut(cacheNames="users",key="#record.id") public User updateByPrimaryKeySelective(User record) { userMapper.updateByPrimaryKeySelective(record); return userMapper.selectByPrimaryKey(record.getId()); } @Override @CacheEvict(cacheNames="users",key="#id") public int deleteByPrimaryKey(Long id) { return userMapper.deleteByPrimaryKey(id); } }
这里有必要说明一下spring cache相关的注解:
- @CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置
@Cacheable:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:
value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了
key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = “#p0”):使用函数第一个参数作为缓存的key值
- condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = “#p0”, condition = “#p0.length() < 3”),表示只有当第一个参数的长度小于3的时候才会被缓存
- unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
- keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
- cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
- cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
@CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析
- @CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:
- allEntries:非必需,默认为false。当为true时,会移除所有数据
- beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
参考链接如下:
UserController,代码如下:
package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import com.example.demo.model.User; import com.example.demo.service.UserService; @Controller public class UserController { @Autowired private UserService userService; @RequestMapping("/add-user") @ResponseBody public String insertSelective(User record) { userService.insertSelective(record); return "OK"; } @RequestMapping("/get-by-id") @ResponseBody public User selectByPrimaryKey(Long id) { return userService.selectByPrimaryKey(id); } @RequestMapping("/update-by-id") @ResponseBody public String update(User record) { userService.updateByPrimaryKeySelective(record); return "ok"; } @RequestMapping("/delete-by-id") @ResponseBody public Integer delete(Long id) { return userService.deleteByPrimaryKey(id); } }
测试:
首先我们访问如下链接:
http://127.0.0.1:8080/add-user?nickName=harry&emailNew=xxx@xxx.com
http://127.0.0.1:8080/add-user?nickName=harry2&emailNew=xxx@xxx.com
加入2条数据
然后我们访问如下链接:
http://127.0.0.1:8080/get-by-id?id=1
发现有打印日志,如下:
2018-01-23 14:52:41.658 INFO 60766 --- [nio-8881-exec-6] c.example.demo.service.UserServiceImpl : 查询数据库
返回值如下:
{ id: 1, nickName: "harry", emailNew: "xxx@xxx.com", registerTime: 1484488802000 }
此时我们再次访问,发现没有再次打印日志,而是直接从缓存中返回的.
访问如下链接就行修改:
http://127.0.0.1:8080/update-by-id?id=1&nickName=22
将昵称改为22.
此时我们继续访问 http://127.0.0.1:8080/get-by-id?id=1 ,发现没有打印日志,且返回的结果中昵称也改为了22,如下:
{ id: 1, nickName: "22", emailNew: "xxx@xxx.com", registerTime: 1484488802000 }
ConcurrentMapCache解析
ConcurrentMapCache的自动装配声明在SimpleCacheConfiguration中.
SimpleCacheConfiguration 声明了如下注解:
@Configuration @ConditionalOnMissingBean(CacheManager.class) @Conditional(CacheCondition.class)
- @Configuration –> 配置类
- @ConditionalOnMissingBean(CacheManager.class)–> 当BeanFactory中缺少CacheManager类型的bean时生效
@Conditional(CacheCondition.class) –> 通过CacheCondition来进行判断,代码如下:
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { // 1. 获得被注解的类名 String sourceClass = ""; if (metadata instanceof ClassMetadata) { sourceClass = ((ClassMetadata) metadata).getClassName(); } ConditionMessage.Builder message = ConditionMessage.forCondition("Cache", sourceClass); // 2. 实例化RelaxedPropertyResolver,读取spring.cache.开头的属性 RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( context.getEnvironment(), "spring.cache."); // 3. 如果没有配置spring.cache.type,则返回匹配--> 进行自动选择 if (!resolver.containsProperty("type")) { return ConditionOutcome.match(message.because("automatic cache type")); } // 4. 根据被注解的类名获得对应的CacheType CacheType cacheType = CacheConfigurations .getType(((AnnotationMetadata) metadata).getClassName()); // 5. 将spring.cache.type 配置的值 中的-变为_,然后变成大写,如果和CacheType的name相等的化,则返回匹配,否则,返回不匹配 String value = resolver.getProperty("type").replace('-', '_').toUpperCase(); if (value.equals(cacheType.name())) { return ConditionOutcome.match(message.because(value + " cache type")); } return ConditionOutcome.noMatch(message.because(value + " cache type")); }
- 获得被注解的类名,对于当前是org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
- 实例化RelaxedPropertyResolver,读取spring.cache.开头的属性
- 如果没有配置spring.cache.type,则返回匹配–> 进行自动选择. 由于默认情况下是没有配置spring.cache.type的,因此,在这里返回.
- 根据被注解的类名获得对应的CacheType
- 将spring.cache.type 配置的值 中的-变为_,然后变成大写,如果和CacheType的name相等的化,则返回匹配,否则,返回不匹配
bean方法,代码如下:
@Bean public ConcurrentMapCacheManager cacheManager() { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); List<String> cacheNames = this.cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { cacheManager.setCacheNames(cacheNames); } return this.customizerInvoker.customize(cacheManager); }
- @Bean –> 注册1个id为cacheManager,类型为ConcurrentMapCacheManager的bean
该方法的处理逻辑如下:
- 实例化ConcurrentMapCacheManager
- 如果配置有spring.cache.cache-names=xx,xx,则进行配置cacheNames,默认是没有配置的
- 调用CacheManagerCustomizers#customize 进行个性化设置,在该方法中是遍历其持有的List